def mysum(a,b):
= a + b
c return c
L05: Functions, packages
Lecture Overview
In this lecture we introduce “functions” which give us a way to re-use a piece of code without having to type it all out every time we need to use it. We show how to construct functions (“define” them) and how to use them (“call” them).
We then introduce “packages”, which give us a way to bundle multiple functions under a single name.
Finally, we show how functions and packages give us a way to chain together multiple Python commands on a single line of code.
Functions
Functions are pieces of code with a name. The general syntax for creating a function is as follow:
def <functionName>(<functionArguments>):
<statement(s)>
return <value>
Declared as above, functions take in some data (<functionArguments>), perform some calculation based on that data (<statement(s)>), and output the results of that calculation to be used later on in the program (return <>).
We will see below, that one can omit the function arguments, the statements in the body of the function, or the return statement at the end (though not all at once).
Here is a very simple example of a function that calculates the sum of its arguments and outputs (returns) the value of that sum:
Note that the above code only “defines” what the function “mysum” does, but it does not actually “do” anything (no calculation is actually performed). To make the function do something, we need to use it in an expression (this is referred to as “calling” the function):
print(mysum(1,2))
3
= mysum(2,5)
d print(d)
7
Some functions have no arguments:
def f():
print("abc")
return 25
f()
abc
25
= f() c
abc
print(c)
25
Some functions have no statements in their body:
def mysum2(a,b):
return a+b
print(mysum2(3,5))
8
Some functions have no return statement:
def myprint(a,b,c):
print(a+b+c)
1,2,3) myprint(
6
'a','b','c') myprint(
abc
1,2],['b'],[]) myprint([
[1, 2, 'b']
Functions without a return statements, do actually return something: the Python value “None”:
= myprint(1,2,3)
c print(c)
6
None
When a function executes a return statement, it does not execute any remaining code in the body of the function (i.e. it exits):
def func():
return 2
print("123")
func()
2
Positional vs keyword arguments
When you call (use) a function, you can provide arguments based on their name (keyword arguments), or based on their position (positional arguments)
def msg(name,age):
print(f"{name} is {age} years old")
Using positional arguments:
"Mihai", 104) msg(
Mihai is 104 years old
Using keyword arguments:
= "Mihai", age = 104) msg(name
Mihai is 104 years old
The benefit of using keyword arguments is that you don’t have to remember their order in the function definition, and accidentally do something like this:
104,"Mihai") msg(
104 is Mihai years old
As long as you remember the keywords (“age” and “name” in our example), you don’t have to worry about the order in which you supply them to the function call:
= 104, name = "Mihai") msg(age
Mihai is 104 years old
The drawback of keyword arguments is that you have to remember the keywords:
= 5, n = "Mihai") msg(age
TypeError: msg() got an unexpected keyword argument 'n'
Required vs optional parameters
Regardless of whether we use positional or keyword arguments, in the examples above, we always had to supply the correct number of arguments:
"mihai",5, [123]) msg(
TypeError: msg() takes 2 positional arguments but 3 were given
= 'mihai') msg(name
TypeError: msg() missing 1 required positional argument: 'age'
To get around this issue, we can supply default values for some of the parameters when we define the function, even if these default values are “empty” (using the None value):
def msg2(name, age = 12):
print(f"{name} is {age} years old")
= "mihai") msg2(name
mihai is 12 years old
If we don’t have to give a meaningful default value to a parameter, we can use the “None” value:
def msg2(name, age = None):
print(f"{name} is {age} years old")
'mihai') msg2(
mihai is None years old
The one rule you need to remember about optional parameters (ones with a default value) is that they need to be specified AFTER all the require parameters:
def msg3(name = '', age):
print('this will not work')
SyntaxError: non-default argument follows default argument (Temp/ipykernel_15972/4155387656.py, line 1)
Advanced properties of functions (OPTIONAL)
Side-effects
In some instances, functions can alter the value of the objects used as their parameters:
def changeme(x):
0] = "weird" x[
= [1,2,3]
mylist
changeme(mylist)print(mylist)
['weird', 2, 3]
In some instances, functions can NOT alter the value of their arguments:
def changeme2(x):
= "weird" x
= 12
mylist
changeme2(mylist)print(mylist)
12
As a general rule, functions can modify the values of arguments of mutable type (e.g. list, dict) but not the values of arguments of immutable type (e.g. int, str, tuple). If you are interested in more details, a nice discussion is provided here: https://realpython.com/defining-your-own-python-function/#argument-passing
Variable-length argument lists
You don’t have to decide how many arguments your function should take when you define that function. For example, the function below can take in ANY number of arguments and prints them all out:
def printme(*args):
print(args)
1,2,3) printme(
(1, 2, 3)
1,2,3,4,5) printme(
(1, 2, 3, 4, 5)
We will not be using this functionality in this course but you can read more about it here: https://realpython.com/defining-your-own-python-function/#variable-length-argument-lists
Packages
Python allows us to use functions defined in a different location. As long as the function we want to use is in a “.py” file somewhere on your computer, you should be able to “import” it in the current file so you can use it.
For example, in the same folder as these lectures, you should have a file called “MyPackage.py” (though you may not see the “.py”), which contains two functions: mylist(), and myprint(). We can use those functions here, by just importing “MyPackage”:
import MyPackage
Now we can use the functions inside “MyPackage” using the dot notation (familiar from when we introduced object attributes):
= MyPackage.mylist(2,5)
p print(p)
[2, 3, 4, 5]
MyPackage.myprint()
abc
Often times, we rename a package (right after we import it) to a shorter name (to reduce typing):
import MyPackage as mp
mp.myprint()
abc
We can also import all the functions inside a package in such a way that we don’t have to keep specifying the package name every time we use the function (though this approach is not recommended):
from MyPackage import *
myprint()
abc
Packages vs modules vs scripts (OPTIONAL)
Some terminology:
Python scripts are “.py” files that are primarily meant to be run.
Python modules are “.py” files that are primarily meant to be imported.
Python packages are collections of Python modules.
So, technically speaking, the “MyPackage.py” file is a module.
You do not have to remember this terminology for the purpose of this class. We will USE packages that other people have written, but, to keep things simple, we will not write any scripts, modules, or packages of our own: we will execute all our code inside Jupyter Notebooks like this one.
However, if you are serious about Python programming beyond the scope of this class, it is very important to understand exactly how packages, modules, and scripts work. A nice discussion can be found here: https://realpython.com/python-modules-packages/
Chaining commands together
Whenever a function in Python returns a value, we can operate on that value immediately (in the same line of code as the function call).
For example the following code:
= mp.mylist(1,2)
ml print(ml[-1])
2
Is equivalent to this:
print(mp.mylist(1,2)[-1])
2
Python first runs the mylist function, which returns the list [1,2]:
1,2) mp.mylist(
[1, 2]
And then applies the [-1] slice to that list, running something equivalent to:
print([1,2][-1])
2
We can also chain multiple function calls together:
5,13).__len__() mp.mylist(
9
This works because the output of mp.mylist(5,13)
is a list, and lists have an attribute called __len__()
which returns the number of elements of a list.
We can chain as many function/attribute calls as we want on a single line of code. Python will execute function/attribute calls from left to right on any given line of code. So we have to make sure that, whenever we use an attribute, all the code on its left produces output of a type that has the attribute we want to use.
For example, in the line below:
5,13).__len__().__sizeof__() mp.mylist(
28
We saw that mp.mylist(5,13).__len__()
produces an output of type int (the number 9):
print(type(mp.mylist(5,13).__len__()))
<class 'int'>
And objects of type int, have __sizeof__
as an attribute (which gives us the amount of memory that int is using):
print(dir(int))
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
If __sizeof__
was not in the list above, we could have not applied .__sizeof__()
after mp.mylist(5,13).__len__()
Finally, mp.mylist(5,13).__len__().__sizeof__()
shows us that the dot can be used to separate package names (mp), user-written functions (mylist) as well as built-in attributes (__len__
and __sizeof__
)